iT邦幫忙

2024 iThome 鐵人賽

DAY 24
0

現在已經有兩個關鍵元件齊備:新型態的代理人(ActionClient),它和 server 之間有不同的狀態機來回,而且已經可以接起來。有趣的是,我並非完全採用隨機選點的方式,而是一律從所有選點當中持續從頭到尾的順序反覆嘗試,這會讓它的勝率比隨機猴子還差,不過無所謂,目的是有這個可以遊玩的狀態機。第二個元件是網頁邏輯和基本的輸入資訊,來自棋譜閱讀器,已經可以在瀏覽器的 console 觀察到不同座標的點擊,但是還不確定這個資訊怎麼傳回到 Rust 的部份。

看似準備蠻齊全了,但是說實在話還是不知道怎麼整合。主要是後者,因為要造出一個 HTTP 伺服器的緣故,有個卡住等待的邏輯,

    // Start the service
    HttpServer::new(move || {
        App::new()
            .app_data(setup_state.clone())
            .route("/", web::get().to(index))
            .route("/game_state", web::get().to(game_state))
            .service(fs::Files::new("/static", "static").show_files_listing())
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await

但因為我們的伺服器可以主持多場遊戲,所以我希望這些展開伺服器連上網頁遊玩結束遊戲再啟新局的過程,可以安插在新的代理人的某些迴圈裡面。對於 Rust 的 actix 非同步框架毫不熟悉的我,於是直接走上詠唱之道。

詠唱全文(對象:ChatGPT o1-mini)

You are a Rust hacker and you are implementing a simple framework according to my spec. The spec is rough, but the rule of thumb is as simple as possible.

Context:

  1. There is a chess client program. Don't bother consider the implementation. I got it done already. But it currently accepts only standard input from command line. This project (your assignment) is to upgrade this project with a Web interface.
  2. The chess client program has a main loop that can initialize multiple games. Don't bother how it connects to server, what protocol, etc., because I have done them already. But, every now and then a new game starts, I want the corresponding Web interface (for a upgraded game play experience) to have a short delay, and then reload itself to prepare for the new game. ...... However, this is just my naive intuition. Maybe a better way is just to shutdown the web server with something like actix oneshot mechanism. I don't know. Please advise the most simple yet effective (by effective, I mean, don't give me code that has dependency problems).

What I want you to implement:

  1. A mock client program, main.rs, in Rust. It can do nothing important, but has a main loop structure to mock multiple games.
  2. A web_server.rs. It contains the web server that can host a index.html, with some simple function in app.js that translates the mouse click events on a 2x2 grid in the web page into coordinates back to somewhere in the web_server.rs, and thus (this is important) the information can be passed back to main.rs.

Note that the code you give me will compile without problem, so you have to give me a well-crafted and well-tested Cargo.toml manifest as well. The simple framework will be brief yet comprehensive, and you expect me to smoothly integrate this framework into what I currently have.

Thank you in advance.

結果

儘管耳提面命,它還是造成了一些錯誤,但是加一加套件與宣告,就整組動起來了。有興趣的讀者可以直接抄過去用:

目錄結構

chess_web_interface/
├── Cargo.toml
└── src
    ├── main.rs
    └── web_server.rs
└── static
    ├── index.html
    └── app.js

Cargo.toml

[package]
name = "fake"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4"
actix-files = "0.6.6"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

src/main.rs

// src/main.rs

use std::sync::mpsc::{self, Receiver, Sender};
use std::thread;
use std::time::Duration;

mod web_server;
use web_server::start_web_server;

fn main() {
    // Channel to send reload signals to the web server
    let (reload_tx, reload_rx): (Sender<()>, Receiver<()>) = mpsc::channel();

    // Channel to receive click coordinates from the web server
    let (click_tx, click_rx): (Sender<(u32, u32)>, Receiver<(u32, u32)>) = mpsc::channel();

    // Start the web server in a separate thread
    let web_server_handle = thread::spawn(move || {
        start_web_server(reload_rx, click_tx);
    });

    // Main loop to simulate multiple games
    for game_number in 1..=5 {
        println!("Starting Game {}", game_number);
        // Simulate game duration
        let game_duration = Duration::from_secs(10);
        let start_time = std::time::Instant::now();

        loop {
            // Check if the game duration has passed
            if start_time.elapsed() >= game_duration {
                println!("Game {} ended.", game_number);
                // Signal the web server to reload for the next game
                reload_tx.send(()).expect("Failed to send reload signal");
                break;
            }

            // Check for any click events from the web interface
            if let Ok((x, y)) = click_rx.try_recv() {
                println!("Received click at ({}, {}) from web interface.", x, y);
                // Here you can handle the click coordinates as needed
            }

            // Sleep briefly to avoid busy waiting
            thread::sleep(Duration::from_millis(100));
        }

        // Wait before starting the next game
        thread::sleep(Duration::from_secs(2));
    }

    // After all games, you can shutdown the web server if needed
    // For simplicity, we'll just exit
    println!("All games completed. Shutting down.");
    // Note: In a real application, you would implement a graceful shutdown
    // mechanism for the web server.

    // For this mock, we'll simply exit.
}

src/web_server.rs

// src/web_server.rs

use actix_files as fs;
use actix_web::rt::System;
use actix_web::{post, web, App, HttpResponse, HttpServer, Responder};
use serde::Deserialize;
use std::sync::mpsc::{Receiver, Sender};
use std::thread;

#[derive(Deserialize)]
struct ClickData {
    x: u32,
    y: u32,
}

// Handler for receiving click coordinates
#[post("/click")]
async fn receive_click(data: web::Json<ClickData>, click_tx: web::Data<Sender<(u32, u32)>>) -> impl Responder {
    let x = data.x;
    let y = data.y;
    // Send the coordinates back to main.rs
    if let Err(e) = click_tx.send((x, y)) {
        eprintln!("Failed to send click data to main: {}", e);
        return HttpResponse::InternalServerError().body("Failed to process click");
    }
    HttpResponse::Ok().body("Click received")
}

pub fn start_web_server(reload_rx: Receiver<()>, click_tx: Sender<(u32, u32)>) {
    // Clone the click_tx to move into the Actix-web data
    let click_tx_data = web::Data::new(click_tx);

    // Start the Actix system
    let server = HttpServer::new(move || {
        App::new()
            .app_data(click_tx_data.clone())
            .service(receive_click)
            .service(fs::Files::new("/", "./static").index_file("index.html"))
    })
    .bind(("127.0.0.1", 8080))
    .expect("Can not bind to port 8080")
    .run();

    // Handle reload signals in a separate thread
    let reload_handle = thread::spawn(move || {
        for _ in reload_rx {
            println!("Reload signal received. Reloading the web interface...");
            // Implement the reload logic here.
            // For simplicity, we'll print a message.
            // In a real application, you might notify the frontend to reload via WebSockets
            // or another mechanism.
            // Alternatively, you could restart the server, but that's more involved.
        }
    });

    // Run the server (this blocks until the server is stopped)
    let server_handle = thread::spawn(move || {
        let sys = actix_web::rt::System::new();
        sys.block_on(server)
    });

    // Wait for both threads to finish
    server_handle.join().expect("Server thread panicked");
    reload_handle.join().expect("Reload thread panicked");
}

static/index.html

<!-- static/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Chess Web Interface</title>
    <style>
        #grid {
            display: grid;
            grid-template-columns: repeat(2, 100px);
            grid-template-rows: repeat(2, 100px);
            gap: 10px;
            margin: 50px auto;
            width: max-content;
        }
        .cell {
            background-color: #f0f0f0;
            border: 1px solid #ccc;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            font-size: 24px;
        }
        .cell:hover {
            background-color: #e0e0e0;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">Chess Web Interface</h1>
    <div id="grid">
        <div class="cell" data-x="0" data-y="0">A1</div>
        <div class="cell" data-x="1" data-y="0">B1</div>
        <div class="cell" data-x="0" data-y="1">A2</div>
        <div class="cell" data-x="1" data-y="1">B2</div>
    </div>
    <script src="app.js"></script>
</body>
</html>

static/app.js

// static/app.js

document.addEventListener('DOMContentLoaded', () => {
    const cells = document.querySelectorAll('.cell');

    cells.forEach(cell => {
        cell.addEventListener('click', () => {
            const x = parseInt(cell.getAttribute('data-x'));
            const y = parseInt(cell.getAttribute('data-y'));

            fetch('/click', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ x, y }),
            })
            .then(response => response.text())
            .then(data => {
                console.log('Server response:', data);
            })
            .catch((error) => {
                console.error('Error:', error);
            });
        });
    });
});

運行結果

https://ithelp.ithome.com.tw/upload/images/20240924/20103524LA40ChZstz.png

真的會顯示這個可以按的樣子!然後也的確可以看到 Rust 端能夠得到相關資訊,如:

https://ithelp.ithome.com.tw/upload/images/20240924/20103524OLRIyouJ2c.png

實在是...太好用了。所以接下來就是用這套串接的方法來看怎麼串我原本有的東西囉。


上一篇
回顧訓練歷程 3/3
下一篇
對戰網頁實作:多串一點點
系列文
DeltaPathogen:國產雙人不對稱抽象棋「疫途」之桌遊 AI 實戰30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言